package dproxy.application.ui;

import java.io.BufferedReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Map.Entry;

import dproxy.application.ui.commands.Command;
import dproxy.application.ui.commands.Delete;
import dproxy.application.ui.commands.Detail;
import dproxy.application.ui.commands.List;
import dproxy.application.ui.commands.Switch;
import dproxy.application.ui.commands.ValidationException;
import dproxy.server.cache.dao.DatabaseImplentation;
import dproxy.server.common.exceptions.AppInfrastructureException;

/**
 * Read shell input and process commands.
 */
public final class CommandLineProcessor  {

    /** Singleton instance. */
    private static CommandLineProcessor instance = new CommandLineProcessor();
    /** Input reader. */
    private BufferedReader inputReader = new BufferedReader(new InputStreamReader(System.in));
    /** Default cache implementation is persistent. Can be changed by command. */
    private DatabaseImplentation cacheImplementation =  DatabaseImplentation.PERSISTENT;
    
    /**
     * @return Singleton instance
     */
    public static CommandLineProcessor getInstance() {
        return instance;
    }
    
    /**
     * Private constructor.
     */
    private CommandLineProcessor() {
        
    }
    
    /**
     * Start command processing.
     */
    public void run() {
        try {
            // Get all available commands
            Collection <Class> commands = getCommands();
            Map <String, Class> commandNames = getCommandNames(commands);
            
            write("[CommandLineProcessor started - enter 'help' for available commands]");
            while (true) {
                String input = read();
                if (!input.equals("")) {
                    // Process commands:
                    StringTokenizer tokenizer = new StringTokenizer(input, " ");
                    String commandName = tokenizer.nextToken().trim();
                    
                    if (commandName.equalsIgnoreCase("stop") 
                        || commandName.equalsIgnoreCase("kill")
                        || commandName.equalsIgnoreCase("quit")
                        || commandName.equalsIgnoreCase("halt")
                        || commandName.equalsIgnoreCase("exit")) {
                        // Finish loop, which will return control to the application.
                        // This will stop the servers
                      break;
                    } else if (commandName.equalsIgnoreCase("help")) {
                        // Print help to the output
                        write("   'stop'       - Close server");
                        for (Entry <String, Class> commandEntry : commandNames.entrySet()) {
                            Command command = (Command) commandEntry.getValue().newInstance();
                            write(command.description());
                        }
                    } else {
                        if (commandNames.containsKey(commandName)) {
                            // Execute command
                            Command command = (Command) commandNames.get(commandName).newInstance();
                            Collection <String> parameters = new ArrayList <String>();
                            while (tokenizer.hasMoreTokens()) {
                                parameters.add(tokenizer.nextToken());
                            }
                            try {
                                command.validate(parameters);
                                // Confirm before execution
                                if (command.confirm()) {
                                    try {
                                        command.execute();
                                    } catch (Exception e) {
                                        write(e);
                                    }
                                }
                            } catch (ValidationException e) {
                                write("Usage error :"); 
                                write(command.description());
                            }
                        } else {
                            write("Unknown command");
                        }
                    }
                }
            }
        } catch (InstantiationException e) {
            throw new AppInfrastructureException(e);
        } catch (IllegalAccessException e) {
            throw new AppInfrastructureException(e);
        } 
    }

    /**
     * @param commands Set of all Command implementing classes
     * @return Map containing {commandname, CommandImplementingClass} pairs
     * CommandName is the lowercase of the class that implements it.
     * For instance, if a command is implemented by a class named Delete,
     * it's command name will be delete
     */
    private Map <String, Class> getCommandNames(Collection<Class> commands) {
        Map <String, Class> classNames = new HashMap <String, Class>();
        for (Class className : commands) {
            classNames.put(className.getSimpleName().toLowerCase(), className);
        }
        return classNames;
    }

    /**
     * All available commands must be added here.
     * @return All available commands
     */
    private Collection <Class> getCommands() {
        Collection <Class> commands = new ArrayList <Class>();

        // Add commands here:
        commands.add(Delete.class);
        commands.add(List.class);
        commands.add(Detail.class);
        commands.add(Switch.class);
        
        return commands;
    }
    
    /**
     * Reads command from input. All reading must be done through this method, even
     * from command executions.
     * @return read command
     */
    public synchronized String read() {
        try {
            return inputReader.readLine().trim();
        } catch (IOException e) {
            throw new AppInfrastructureException(e);
        }
    }
    
    /**
     * Writes a message to the screen. All writing must be done through this method, even
     * from command executions.
     * @param message to be written
     */
    public synchronized void write(String message) {
        System.out.println("- " + message);
    }
    
    /**
     * Writes an error to the screen.
     * @param t error
     */
    public synchronized void write(Throwable t) {
        System.out.println("ERROR: ");
        t.printStackTrace(System.err);
    }

    /**
     * Writes a string to an output file.
     * @param fileName file name
     * @param contents file contents 
     */
    public void writeToFile(String fileName, String contents) {
        try {
            FileWriter writer = new FileWriter(fileName);
            writer.write(contents);
            writer.close();
        } catch (IOException e) {
            write(e);
        }
    }
    
    /* Getters and setters */
    /**
     * @return Returns the cacheImplementation.
     */
    public DatabaseImplentation getCacheImplementation() {
        return cacheImplementation;
    }

    /**
     * @param implementation The cacheImplementation to set.
     */
    public void setCacheImplementation(
            DatabaseImplentation implementation) {
        this.cacheImplementation = implementation;
    }

}
